﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics.Contracts;
using System.IO;

namespace Mobile.Utility.Lib.IO.Compression
{
    public class ZipLibrary : IDisposable
    {
        public ZipLibrary(String path) : this(path, ZipLibraryMode.Read) { }

        public ZipLibrary(String path, ZipLibraryMode mode)
        {
            //relies on File.Open for checking the path argument

            FileMode fileMode;
            FileAccess access;
            switch (mode)
            {
                case ZipLibraryMode.Read:
                    fileMode = FileMode.Open;
                    access = FileAccess.Read;
                    break;
                case ZipLibraryMode.Create:
                    fileMode = FileMode.CreateNew;
                    access = FileAccess.Write;
                    break;
                case ZipLibraryMode.Update:
                    fileMode = FileMode.OpenOrCreate;
                    access = FileAccess.ReadWrite;
                    break;
                default:
                    throw new ArgumentOutOfRangeException("mode");
            }

            FileStream fs = null;
            try
            {
                fs = File.Open(path, fileMode, access);

                Init(fs, mode, false);
            }
            catch
            {
                if (fs != null)
                    fs.Close();
                throw;
            }
        }

        public ZipLibrary(Stream stream) : this(stream, ZipLibraryMode.Read, false) { }

        public ZipLibrary(Stream stream, ZipLibraryMode mode) : this(stream, mode, false) { }

        public ZipLibrary(Stream stream, ZipLibraryMode mode, Boolean leaveOpen)
        {
            if (stream == null)
                throw new ArgumentNullException("stream");
            Contract.EndContractBlock();

            Init(stream, mode, leaveOpen);
        }

        public ReadOnlyCollection<ZipLibraryEntry> ZipItems
        {
            get
            {
                Contract.Ensures(Contract.Result<ReadOnlyCollection<ZipLibraryEntry>>() != null);

                ThrowIfDisposed();

                if (_mode == ZipLibraryMode.Create)
                    throw new NotSupportedException(Mobile.Utility.Lib.IO.Compression.Messages.EntriesInCreateMode);

                EnsureCentralDirectoryRead();
                return _entriesCollection;
            }
        }

        /// <summary>
        /// The ZipLibraryMode that the ZipLibrary was initialized with.
        /// </summary>
        public ZipLibraryMode Mode { get {
            Contract.Ensures(
                   Contract.Result<ZipLibraryMode>() == ZipLibraryMode.Create
                || Contract.Result<ZipLibraryMode>() == ZipLibraryMode.Read
                || Contract.Result<ZipLibraryMode>() == ZipLibraryMode.Update);
            
            return _mode; } }

        /// <summary>
        /// Creates an empty entry in the Zip Library with the specified entry name. There are no restrictions on the names of entries. The last write time of the entry is set to the current time. If an entry with the specified name already exists in the Library, a second entry will be created that has an identical name.
        /// </summary>
        /// <exception cref="ArgumentException">entryName is a zero-length string.</exception>
        /// <exception cref="ArgumentNullException">entryName is null.</exception>
        /// <exception cref="NotSupportedException">The ZipLibrary does not support writing.</exception>
        /// <exception cref="ObjectDisposedException">The ZipLibrary has already been closed.</exception>
        /// <param name="entryName">A path relative to the root of the Library, indicating the name of the entry to be created.</param>
        /// <returns>A wrapper for the newly created file entry in the Library.</returns>
        public ZipLibraryEntry CreateEntry(String entryName)
        {
            Contract.Ensures(Contract.Result<ZipLibraryEntry>() != null);

            ZipHelper.ThrowIfNullOrEmpty(entryName, "entryName");
            ThrowIfDisposed();
            if (_mode == ZipLibraryMode.Read)
                throw new NotSupportedException(Mobile.Utility.Lib.IO.Compression.Messages.CreateInReadMode);

            ZipLibraryEntry entry = new ZipLibraryEntry(this, entryName);
            AddEntry(entry);

            return entry;
        }

        /// <summary>
        /// Retrieves a wrapper for the file entry in the Library with the specified name. Names are compared using ordinal comparison. If there are multiple entries in the Library with the specified name, the first one found will be returned.
        /// </summary>
        /// <exception cref="ArgumentException">entryName is a zero-length string.</exception>
        /// <exception cref="ArgumentNullException">entryName is null.</exception>
        /// <exception cref="NotSupportedException">The ZipLibrary does not support reading.</exception>
        /// <exception cref="ObjectDisposedException">The ZipLibrary has already been closed.</exception>
        /// <exception cref="InvalidDataException">The Zip Library is corrupt and the entries cannot be retrieved.</exception>
        /// <param name="entryName">A path relative to the root of the Library, identifying the desired entry.</param>
        /// <returns>A wrapper for the file entry in the Library. If no entry in the Library exists with the specified name, null will be returned.</returns>
        public ZipLibraryEntry GetEntry(String entryName)
        {
            if (entryName == null)
                throw new ArgumentNullException("entryName");
            Contract.EndContractBlock();

            if (_mode == ZipLibraryMode.Create)
                throw new NotSupportedException(Mobile.Utility.Lib.IO.Compression.Messages.EntriesInCreateMode);

            EnsureCentralDirectoryRead();
            ZipLibraryEntry result;
            _entriesDictionary.TryGetValue(entryName, out result);
            return result;
        }

        /// <summary>
        /// Adds a file from the file system to the Library under the specified entry name. The new entry in the Library will contain the contents of the file. The last write time of the Library entry is set to the last write time of the file on the file system. If an entry with the specified name already exists in the Library, a second entry will be created that has an identical name. If the specified source file has an invalid last modified time, the first datetime representable in the Zip timestamp format (midnight on January 1, 1980) will be used.
        /// </summary>
        /// <exception cref="ArgumentException">sourceFileName is a zero-length string, contains only white space, or contains one or more invalid characters as defined by InvalidPathChars. -or- entryName is a zero-length string.</exception>
        /// <exception cref="ArgumentNullException">sourceFileName or entryName is null.</exception>
        /// <exception cref="PathTooLongException">In sourceFileName, the specified path, file name, or both exceed the system-defined maximum length. For example, on Windows-based platforms, paths must be less than 248 characters, and file names must be less than 260 characters.</exception>
        /// <exception cref="DirectoryNotFoundException">The specified sourceFileName is invalid, (for example, it is on an unmapped drive).</exception>
        /// <exception cref="IOException">An I/O error occurred while opening the file specified by sourceFileName.</exception>
        /// <exception cref="UnauthorizedAccessException">sourceFileName specified a directory. -or- The caller does not have the required permission.</exception>
        /// <exception cref="FileNotFoundException">The file specified in sourceFileName was not found. </exception>
        /// <exception cref="NotSupportedException">sourceFileName is in an invalid format or the ZipLibrary does not support writing.</exception>
        /// <exception cref="ObjectDisposedException">The ZipLibrary has already been closed.</exception>
        /// <param name="sourceFileName">The path to the file on the file system to be copied from. The path is permitted to specify relative or absolute path information. Relative path information is interpreted as relative to the current working directory.</param>
        /// <param name="entryName">The name of the entry to be created.</param>
        /// <returns>A wrapper for the newly created entry.</returns>
        public ZipLibraryEntry CreateEntryFromFile(String sourceFileName, String entryName)
        {
            //argument checking gets passed down to File.Open and CreateEntry
            Contract.Ensures(Contract.Result<ZipLibraryEntry>() != null);

            using (Stream fs = File.Open(sourceFileName, FileMode.Open))
            {
                ZipLibraryEntry entry = CreateEntry(entryName);
                DateTime lastWrite = File.GetLastWriteTime(sourceFileName);
                if (!ZipHelper.IsConvertibleToDosTime(lastWrite))
                    lastWrite = ZipHelper.FirstDosTimeDate;
                entry.LastWriteTime = lastWrite;
                using (Stream es = entry.Open())
                {
                    fs.CopyTo(es);
                }
                return entry;
            }
        }

        /// <summary>
        /// Extracts all of the files in the Library to a directory on the file system. The specified directory must not exist. This method will create all subdirectories and the specified directory. If there is an error while extracting the Library, the Library will remain partially extracted. Each entry will be extracted such that the extracted file has the same relative path to destinationDirectoryName as the entry has to the root of the Library. If a file to be Libraryd has an invalid last modified time, the first datetime representable in the Zip timestamp format (midnight on January 1, 1980) will be used.
        /// </summary>
        /// <exception cref="ArgumentException">destinationDirectoryName is a zero-length string, contains only white space, or contains one or more invalid characters as defined by InvalidPathChars.</exception>
        /// <exception cref="ArgumentNullException">destinationDirectoryName is null.</exception>
        /// <exception cref="PathTooLongException">The specified path, file name, or both exceed the system-defined maximum length. For example, on Windows-based platforms, paths must be less than 248 characters, and file names must be less than 260 characters.</exception>
        /// <exception cref="DirectoryNotFoundException">The specified path is invalid, (for example, it is on an unmapped drive).</exception>
        /// <exception cref="IOException">The directory specified by destinationDirectoryName already exists. -or- An Library entry’s name is zero-length, contains only white space, or contains one or more invalid characters as defined by InvalidPathChars. -or- Extracting an Library entry would have resulted in a destination file that is outside destinationDirectoryName (for example, if the entry name contains parent directory accessors). -or- An Library entry has the same name as an already extracted entry from the same Library.</exception>
        /// <exception cref="UnauthorizedAccessException">The caller does not have the required permission.</exception>
        /// <exception cref="NotSupportedException">destinationDirectoryName is in an invalid format. </exception>
        /// <exception cref="InvalidDataException">An Library entry was not found or was corrupt. -or- An Library entry has been compressed using a compression method that is not supported.</exception>
        /// <param name="destinationDirectoryName">The path to the directory on the file system. The directory specified must not exist. The path is permitted to specify relative or absolute path information. Relative path information is interpreted as relative to the current working directory.</param>
        public void ExtractToDirectory(String destinationDirectoryName)
        {
            //relies on Directory.CreateDirectory for validation of destinationDirectoryName

            if (Directory.Exists(destinationDirectoryName))
            {
                throw new IOException(Mobile.Utility.Lib.IO.Compression.Messages.DirAlreadyExists);
            }

            DirectoryInfo di = Directory.CreateDirectory(destinationDirectoryName);
            String destinationDirectoryFullPath = di.FullName;
            foreach (ZipLibraryEntry entry in ZipItems)
            {
                String fileDestinationPath = Path.GetFullPath(Path.Combine(destinationDirectoryFullPath, entry.FullName));

                if (!fileDestinationPath.StartsWith(destinationDirectoryFullPath, StringComparison.OrdinalIgnoreCase))
                {
                    throw new IOException(Mobile.Utility.Lib.IO.Compression.Messages.ExtractingResultsInOutside);
                }

                //if it is a directory
                if (ZipHelper.EndsWithDirChar(fileDestinationPath))
                {
                    if (entry.Length != 0)
                    {
                        throw new IOException(Mobile.Utility.Lib.IO.Compression.Messages.DirectoryNameWithData);
                    }
                    else
                    {
                        Directory.CreateDirectory(fileDestinationPath);
                    }
                }
                else  //if it is a file
                {
                    //create containing directory
                    Directory.CreateDirectory(Path.GetDirectoryName(fileDestinationPath));
                    entry.ExtractToFile(fileDestinationPath);
                }
            }
        }

        /// <summary>
        /// Finishes writing the Library and releases all resources used by the ZipLibrary object, unless the object was constructed with leaveOpen as true. Any streams from opened entries in the ZipLibrary still open will throw exceptions on subsequent writes, as the underlying streams will have been closed.
        /// </summary>
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        /// <summary>
        /// Releases the unmanaged resources used by ZipLibrary and optionally finishes writing the Library and releases the managed resources.
        /// </summary>
        /// <param name="disposing">true to finish writing the Library and release unmanaged and managed resources, false to release only unmanaged resources.</param>
        protected virtual void Dispose(Boolean disposing)
        {
            if (disposing && !_isDisposed)
            {
                switch (_mode)
                {
                    case ZipLibraryMode.Read:
                        break;
                    case ZipLibraryMode.Create:
                    case ZipLibraryMode.Update:
                    default:
                        Contract.Assert(_mode == ZipLibraryMode.Update || _mode == ZipLibraryMode.Create);
                        try
                        {
                            WriteFile();
                        }
                        catch (InvalidDataException)
                        {
                            CloseStreams();
                            _isDisposed = true;
                            throw;
                        }
                        break;
                }

                CloseStreams();

                _isDisposed = true;
            }
        }

        // Static convenience methods

        /// <summary>
        /// Creates a Zip Library at the path destinationLibrary that contains the files and directories in the directory specified by sourceDirectoryName. The directory structure is preserved in the Library, and a recursive search is done for files to be Libraryd. The Library must not exist. If the directory is empty, an empty Library will be created. If a file in the directory cannot be added to the Library, the Library will be left incomplete and invalid and the method will throw an exception. This method optionally includes the base directory in the Library. If an error is encountered while adding files to the Library, this method will stop adding files and leave the Library in an invalid state. The paths are permitted to specify relative or absolute path information. Relative path information is interpreted as relative to the current working directory. If a file in the Library has data in the last write time field that is not a valid Zip timestamp, an indicator value of 1980 January 1 at midnight will be used for the file’s last modified time.
        /// </summary>
        /// <exception cref="ArgumentException">sourceDirectoryName or destinationLibrary is a zero-length string, contains only white space, or contains one or more invalid characters as defined by InvalidPathChars.</exception>
        /// <exception cref="ArgumentNullException">sourceDirectoryName or destinationLibrary is null.</exception>
        /// <exception cref="PathTooLongException">In sourceDirectoryName or destinationLibrary, the specified path, file name, or both exceed the system-defined maximum length. For example, on Windows-based platforms, paths must be less than 248 characters, and file names must be less than 260 characters.</exception>
        /// <exception cref="DirectoryNotFoundException">The path specified in sourceDirectoryName or destinationLibrary is invalid, (for example, it is on an unmapped drive). -or- The directory specified by sourceDirectoryName does not exist.</exception>
        /// <exception cref="IOException">destinationLibrary exists. -or- An I/O error occurred while opening a file to be Libraryd.</exception>
        /// <exception cref="UnauthorizedAccessException">destinationLibrary specified a directory. -or- The caller does not have the required permission.</exception>
        /// <exception cref="NotSupportedException">sourceDirectoryName or destinationLibrary is in an invalid format.</exception>
        /// <param name="sourceDirectoryName">The path to the directory on the file system to be Libraryd.</param>
        /// <param name="destinationLibrary">The name of the Library to be created.</param>
        /// <param name="includeBaseDirectory">True to indicate that a directory named sourceDirectoryName should be included at the root of the Library. False to indicate that the files and directories in sourceDirectoryName should be included directly in the Library.</param>
        public static void CreateFromDirectory(String sourceDirectoryName, String destinationLibrary, Boolean includeBaseDirectory)
        {
            //relies on Path.GetFullPath for validation of sourceDirectoryName and destinationLibrary

            sourceDirectoryName = Path.GetFullPath(sourceDirectoryName);
            destinationLibrary = Path.GetFullPath(destinationLibrary);

            sourceDirectoryName = sourceDirectoryName.TrimEnd(Path.DirectorySeparatorChar,
                                                                Path.AltDirectorySeparatorChar);
            Contract.Assert(sourceDirectoryName.Length != 0);

            using (ZipLibrary Library = new ZipLibrary(destinationLibrary, ZipLibraryMode.Create))
            {
                String baseDirectory = sourceDirectoryName;
                if (includeBaseDirectory)
                {
                    //walks up one directory, so that we chop off one less directory
                    baseDirectory = Path.GetDirectoryName(baseDirectory);
                }
                Int32 basePathLength = baseDirectory.Length + 1;

                //add files and directories
                DirectoryInfo di = new DirectoryInfo(sourceDirectoryName);

                foreach (FileSystemInfo file in di.EnumerateFileSystemInfos("*", SearchOption.AllDirectories))
                {
                    String fileFullPath = file.FullName;
                    Int32 entryNameLength = fileFullPath.Length - basePathLength;

                    Contract.Assert(entryNameLength > 0);

                    if (file is FileInfo)
                    {
                        Library.CreateEntryFromFile(file.FullName, fileFullPath.Substring(basePathLength, entryNameLength));
                    }
                    else
                    {
                        DirectoryInfo possiblyEmpty = file as DirectoryInfo;
                        if (possiblyEmpty != null)
                        {
                            if (ZipHelper.IsDirEmpty(possiblyEmpty))
                            {
                                /* FullName never returns a directory separator character on the end, but
                                 * Zip Librarys require it to specify an explicit directory
                                 */
                                Library.CreateEntry(fileFullPath.Substring(basePathLength, entryNameLength) + Path.DirectorySeparatorChar);
                            }
                        }
                    }
                }
            }
        }

        /// <summary>
        /// Creates a Zip Library at the path destinationLibrary that contains the files and directories in the directory specified by sourceDirectoryName. The directory structure is preserved in the Library, and a recursive search is done for files to be Libraryd. The Library must not exist. If the directory is empty, an empty Library will be created. If a file in the directory cannot be added to the Library, the Library will be left incomplete and invalid and the method will throw an exception. This method does not include the base directory in the Library. If an error is encountered while adding files to the Library, this method will stop adding files and leave the Library in an invalid state. The paths are permitted to specify relative or absolute path information. Relative path information is interpreted as relative to the current working directory. If a file in the Library has data in the last write time field that is not a valid Zip timestamp, an indicator value of 1980 January 1 at midnight will be used for the file’s last modified time.
        /// </summary>
        /// <exception cref="ArgumentException">sourceDirectoryName or destinationLibrary is a zero-length string, contains only white space, or contains one or more invalid characters as defined by InvalidPathChars.</exception>
        /// <exception cref="ArgumentNullException">sourceDirectoryName or destinationLibrary is null.</exception>
        /// <exception cref="PathTooLongException">In sourceDirectoryName or destinationLibrary, the specified path, file name, or both exceed the system-defined maximum length. For example, on Windows-based platforms, paths must be less than 248 characters, and file names must be less than 260 characters.</exception>
        /// <exception cref="DirectoryNotFoundException">The path specified in sourceDirectoryName or destinationLibrary is invalid, (for example, it is on an unmapped drive). -or- The directory specified by sourceDirectoryName does not exist.</exception>
        /// <exception cref="IOException">destinationLibrary exists. -or- An I/O error occurred while opening a file to be Libraryd.</exception>
        /// <exception cref="UnauthorizedAccessException">destinationLibrary specified a directory. -or- The caller does not have the required permission.</exception>
        /// <exception cref="NotSupportedException">sourceDirectoryName or destinationLibrary is in an invalid format.</exception>
        /// <param name="sourceDirectoryName">The path to the directory on the file system to be Libraryd. </param>
        /// <param name="destinationLibrary">The name of the Library to be created.</param>
        public static void CreateFromDirectory(String sourceDirectoryName, String destinationLibrary)
        {
            CreateFromDirectory(sourceDirectoryName, destinationLibrary, false);
        }

        /// <summary>
        /// Extracts all of the files in the specified Library to a directory on the file system. The specified directory must not exist. This method will create all subdirectories and the specified directory. If there is an error while extracting the Library, the Library will remain partially extracted. Each entry will be extracted such that the extracted file has the same relative path to the destinationDirectoryName as the entry has to the Library. The path is permitted to specify relative or absolute path information. Relative path information is interpreted as relative to the current working directory. If a file to be Libraryd has an invalid last modified time, the first datetime representable in the Zip timestamp format (midnight on January 1, 1980) will be used.
        /// </summary>
        /// <exception cref="ArgumentException">sourceLibrary or destinationDirectoryName is a zero-length string, contains only white space, or contains one or more invalid characters as defined by InvalidPathChars.</exception>
        /// <exception cref="ArgumentNullException">sourceLibrary or destinationDirectoryName is null.</exception>
        /// <exception cref="PathTooLongException">sourceLibrary or destinationDirectoryName specifies a path, file name, or both exceed the system-defined maximum length. For example, on Windows-based platforms, paths must be less than 248 characters, and file names must be less than 260 characters.</exception>
        /// <exception cref="DirectoryNotFoundException">The path specified by sourceLibrary or destinationDirectoryName is invalid, (for example, it is on an unmapped drive).</exception>
        /// <exception cref="IOException">The directory specified by destinationDirectoryName already exists. -or- An I/O error has occurred. -or- An Library entry’s name is zero-length, contains only white space, or contains one or more invalid characters as defined by InvalidPathChars. -or- Extracting an Library entry would result in a file destination that is outside the destination directory (for example, because of parent directory accessors). -or- An Library entry has the same name as an already extracted entry from the same Library.</exception>
        /// <exception cref="UnauthorizedAccessException">The caller does not have the required permission.</exception>
        /// <exception cref="NotSupportedException">sourceLibrary or destinationDirectoryName is in an invalid format. </exception>
        /// <exception cref="FileNotFoundException">sourceLibrary was not found.</exception>
        /// <exception cref="InvalidDataException">The Library specified by sourceLibrary: Is not a valid ZipLibrary -or- An Library entry was not found or was corrupt. -or- An Library entry has been compressed using a compression method that is not supported.</exception>
        /// <param name="sourceLibrary">The path to the Library on the file system that is to be extracted.</param>
        /// <param name="destinationDirectoryName">The path to the directory on the file system. The directory specified must not exist, but the directory that it is contained in must exist.</param>
        public static void ExtractToDirectory(String sourceLibrary, String destinationDirectoryName)
        {
            using (ZipLibrary Library = new ZipLibrary(sourceLibrary))
            {
                Library.ExtractToDirectory(destinationDirectoryName);
            }
        }

        #region private properties

        internal Stream LibraryStream { get { return _LibraryStream; } }
        internal BinaryReader LibraryReader { get { return _LibraryReader; } }

        internal UInt32 NumberOfThisDisk { get { return _numberOfThisDisk; } }

        #endregion

        #region private/internal methods

        private void Init(Stream stream, ZipLibraryMode mode, Boolean leaveOpen)
        {
            Stream extraTempStream = null;

            try
            {
                _backingStream = null;

                //check stream against mode
                switch (mode)
                {
                    case ZipLibraryMode.Create:
                        if (!stream.CanWrite)
                            throw new ArgumentException(Mobile.Utility.Lib.IO.Compression.Messages.CreateModeCapabilities);
                        break;
                    case ZipLibraryMode.Read:
                        if (!stream.CanRead)
                            throw new ArgumentException(Mobile.Utility.Lib.IO.Compression.Messages.ReadModeCapabilities);
                        if (!stream.CanSeek)
                        {
                            _backingStream = stream;
                            extraTempStream = stream = new MemoryStream();
                            _backingStream.CopyTo(stream);
                            stream.Seek(0, SeekOrigin.Begin);
                        }
                        break;
                    case ZipLibraryMode.Update:
                        if (!stream.CanRead || !stream.CanWrite || !stream.CanSeek)
                            throw new ArgumentException(Mobile.Utility.Lib.IO.Compression.Messages.UpdateModeCapabilities);
                        break;
                    default:
                        //still have to throw this, because stream constructor doesn't do mode argument checks
                        throw new ArgumentOutOfRangeException("mode");
                }

                _mode = mode;
                _LibraryStream = stream;
                _LibraryStreamOwner = null;
                if (mode == ZipLibraryMode.Create)
                    _LibraryReader = null;
                else
                    _LibraryReader = new BinaryReader(stream);
                _entries = new List<ZipLibraryEntry>();
                _entriesCollection = new ReadOnlyCollection<ZipLibraryEntry>(_entries);
                _entriesDictionary = new Dictionary<String, ZipLibraryEntry>();
                _readEntries = false;
                _leaveOpen = leaveOpen;
                _centralDirectoryStart = 0; //invalid until ReadCentralDirectory
                _isDisposed = false;
                _numberOfThisDisk = 0; //invalid until ReadCentralDirectory
                _LibraryComment = null;

                switch (mode)
                {
                    case ZipLibraryMode.Create:
                        _readEntries = true;
                        break;
                    case ZipLibraryMode.Read:
                        ReadEndOfCentralDirectory();
                        break;
                    case ZipLibraryMode.Update:
                    default:
                        Contract.Assert(mode == ZipLibraryMode.Update);
                        if (_LibraryStream.Length == 0)
                        {
                            _readEntries = true;
                        }
                        else
                        {
                            ReadEndOfCentralDirectory();
                            EnsureCentralDirectoryRead();
                            foreach (ZipLibraryEntry entry in _entries)
                            {
                                entry.ThrowIfNotOpenable(false, true);
                            }
                        }
                        break;
                }
            }
            catch
            {
                if (extraTempStream != null)
                    extraTempStream.Close();

                throw;
            }
        }

        //the only exceptions that this function will throw directly are InvalidDataExceptions
        private void WriteFile()
        {
            //if we are in create mode, we always set readEntries to true in Init
            //if we are in update mode, we call EnsureCentralDirectoryRead, which sets readEntries to true
            Contract.Assert(_readEntries);

            if (_mode == ZipLibraryMode.Update)
            {
                List<ZipLibraryEntry> markedForDelete = new List<ZipLibraryEntry>();
                foreach (ZipLibraryEntry entry in _entries)
                {
                    if (!entry.LoadLocalHeaderExtraFieldAndCompressedBytesIfNeeded())
                        markedForDelete.Add(entry);
                }
                foreach (ZipLibraryEntry entry in markedForDelete)
                    entry.Delete();

                _LibraryStream.Seek(0, SeekOrigin.Begin);
                _LibraryStream.SetLength(0);
                //nothing after this should throw an exception
            }

            foreach (ZipLibraryEntry entry in _entries)
            {
                entry.WriteAndFinishLocalEntry();
            }

            Int64 startOfCentralDirectory = _LibraryStream.Position;

            foreach (ZipLibraryEntry entry in _entries)
            {
                entry.WriteCentralDirectoryFileHeader();
            }

            Int64 sizeOfCentralDirectory = _LibraryStream.Position - startOfCentralDirectory;

            WriteLibraryEpilogue(startOfCentralDirectory, sizeOfCentralDirectory);

        }

        private void CloseStreams()
        {
            if (!_leaveOpen)
            {
                _LibraryStream.Close();
                if (_backingStream != null)
                    _backingStream.Close();
                if (_LibraryReader != null)
                    _LibraryReader.Close();
            }
            else
            {
                /* if _backingStream isn't null, that means we assigned the original stream they passed
                 * us to _backingStream (which they requested we leave open), and _LibraryStream was
                 * the temporary copy that we needed
                 */
                if (_backingStream != null)
                    _LibraryStream.Close();
            }
        }

        //writes eocd, and if needed, zip 64 eocd, zip64 eocd locator
        //should only throw an exception in extremely exceptional cases because it is called from dispose
        private void WriteLibraryEpilogue(Int64 startOfCentralDirectory, Int64 sizeOfCentralDirectory)
        {
            //determine if we need Zip 64
            Boolean needZip64 = false;

            //assume for now that entries can't have more than Int32.Max entries
            if (startOfCentralDirectory > UInt32.MaxValue ||
                sizeOfCentralDirectory > UInt32.MaxValue
#if DEBUG_FORCE_ZIP64
                || _forceZip64
#endif
)
                needZip64 = true;

            //if we need zip 64, write zip 64 eocd and locator
            if (needZip64)
            {
                Int64 zip64EOCDRecordStart = _LibraryStream.Position;

                Zip64EndOfCentralDirectoryRecord.WriteBlock(_LibraryStream, _entries.Count, startOfCentralDirectory, sizeOfCentralDirectory);
                Zip64EndOfCentralDirectoryLocator.WriteBlock(_LibraryStream, zip64EOCDRecordStart);
            }

            //write normal eocd
            ZipEndOfCentralDirectoryBlock.WriteBlock(_LibraryStream, _entries.Count, startOfCentralDirectory, sizeOfCentralDirectory, _LibraryComment);
        }

        private void EnsureCentralDirectoryRead()
        {
            if (!_readEntries)
            {
                ReadCentralDirectory();
                _readEntries = true;
            }
        }

        private void ReadCentralDirectory()
        {
            try
            {
                //assume ReadEndOfCentralDirectory has been called and has populated _centralDirectoryStart

                _LibraryStream.Seek(_centralDirectoryStart, SeekOrigin.Begin);

                Int64 numberOfEntries = 0;

                //read the central directory
                ZipCentralDirectoryFileHeader currentHeader;
                Boolean saveExtraFieldsAndComments = Mode == ZipLibraryMode.Update;
                while (ZipCentralDirectoryFileHeader.TryReadBlock(_LibraryReader,
                                                        saveExtraFieldsAndComments, out currentHeader))
                {
                    AddEntry(new ZipLibraryEntry(this, currentHeader));
                    numberOfEntries++;
                }

                if (numberOfEntries != _expectedNumberOfEntries)
                    throw new InvalidDataException(Mobile.Utility.Lib.IO.Compression.Messages.NumEntriesWrong);
            }
            catch (EndOfStreamException ex)
            {
                throw new InvalidDataException(Mobile.Utility.Lib.IO.Compression.Messages.CentralDirectoryInvalid, ex);
            }
        }

        //This function reads all the EOCD stuff it needs to find the offset to the start of the central directory
        //This offset gets put in _centralDirectoryStart and the number of this disk gets put in _numberOfThisDisk
        //Also does some verification that this isn't a split/spanned Library
        //Also checks that offset to CD isn't out of bounds
        private void ReadEndOfCentralDirectory()
        {
            try
            {
                //this seeks to the start of the end of central directory record
                _LibraryStream.Seek(-ZipEndOfCentralDirectoryBlock.SizeOfBlockWithoutSignature, SeekOrigin.End);
                if (!ZipHelper.SeekBackwardsToSignature(_LibraryStream, ZipEndOfCentralDirectoryBlock.SignatureConstant))
                    throw new InvalidDataException(Mobile.Utility.Lib.IO.Compression.Messages.EOCDNotFound);

                Int64 eocdStart = _LibraryStream.Position;

                //read the EOCD
                ZipEndOfCentralDirectoryBlock eocd;
                Boolean eocdProper = ZipEndOfCentralDirectoryBlock.TryReadBlock(_LibraryReader, out eocd);
                Contract.Assert(eocdProper); //we just found this using the signature finder, so it should be okay

                if (eocd.NumberOfThisDisk != eocd.NumberOfTheDiskWithTheStartOfTheCentralDirectory)
                    throw new InvalidDataException(Mobile.Utility.Lib.IO.Compression.Messages.SplitSpanned);

                _numberOfThisDisk = eocd.NumberOfThisDisk;
                _centralDirectoryStart = eocd.OffsetOfStartOfCentralDirectoryWithRespectToTheStartingDiskNumber;
                if (eocd.NumberOfEntriesInTheCentralDirectory != eocd.NumberOfEntriesInTheCentralDirectoryOnThisDisk)
                    throw new InvalidDataException(Mobile.Utility.Lib.IO.Compression.Messages.SplitSpanned);
                _expectedNumberOfEntries = eocd.NumberOfEntriesInTheCentralDirectory;

                //only bother saving the comment if we are in update mode
                if (_mode == ZipLibraryMode.Update)
                    _LibraryComment = eocd.LibraryComment;

                //only bother looking for zip64 EOCD stuff if we suspect it is needed because some value is FFFFFFFFF
                //because these are the only two values we need, we only worry about these
                //if we don't find the zip64 EOCD, we just give up and try to use the original values
                if (eocd.NumberOfThisDisk == ZipHelper.Mask16Bit ||
                    eocd.OffsetOfStartOfCentralDirectoryWithRespectToTheStartingDiskNumber == ZipHelper.Mask32Bit)
                {
                    //we need to look for zip 64 EOCD stuff
                    //seek to the zip 64 EOCD locator
                    _LibraryStream.Seek(eocdStart - Zip64EndOfCentralDirectoryLocator.SizeOfBlockWithoutSignature, SeekOrigin.Begin);
                    //if we don't find it, assume it doesn't exist and use data from normal eocd
                    if (ZipHelper.SeekBackwardsToSignature(_LibraryStream, Zip64EndOfCentralDirectoryLocator.SignatureConstant))
                    {
                        //use locator to get to Zip64EOCD
                        Zip64EndOfCentralDirectoryLocator locator;
                        Boolean zip64eocdLocatorProper = Zip64EndOfCentralDirectoryLocator.TryReadBlock(_LibraryReader, out locator);
                        Contract.Assert(zip64eocdLocatorProper); //we just found this using the signature finder, so it should be okay

                        if (locator.OffsetOfZip64EOCD > (UInt64)Int64.MaxValue)
                            throw new InvalidDataException(Mobile.Utility.Lib.IO.Compression.Messages.FieldTooBigOffsetToZip64EOCD);
                        Int64 zip64EOCDOffset = (Int64)locator.OffsetOfZip64EOCD;

                        _LibraryStream.Seek(zip64EOCDOffset, SeekOrigin.Begin);

                        //read Zip64EOCD
                        Zip64EndOfCentralDirectoryRecord record;
                        if (!Zip64EndOfCentralDirectoryRecord.TryReadBlock(_LibraryReader, out record))
                            throw new InvalidDataException(Mobile.Utility.Lib.IO.Compression.Messages.Zip64EOCDNotWhereExpected);

                        _numberOfThisDisk = record.NumberOfThisDisk;

                        if (record.NumberOfEntriesTotal > (UInt64)Int64.MaxValue)
                            throw new InvalidDataException(Mobile.Utility.Lib.IO.Compression.Messages.FieldTooBigNumEntries);
                        if (record.OffsetOfCentralDirectory > (UInt64)Int64.MaxValue)
                            throw new InvalidDataException(Mobile.Utility.Lib.IO.Compression.Messages.FieldTooBigOffsetToCD);
                        if (record.NumberOfEntriesTotal != record.NumberOfEntriesOnThisDisk)
                            throw new InvalidDataException(Mobile.Utility.Lib.IO.Compression.Messages.SplitSpanned);

                        _expectedNumberOfEntries = (Int64)record.NumberOfEntriesTotal;
                        _centralDirectoryStart = (Int64)record.OffsetOfCentralDirectory;
                    }
                }

                if (_centralDirectoryStart > _LibraryStream.Length)
                {
                    throw new InvalidDataException(Mobile.Utility.Lib.IO.Compression.Messages.FieldTooBigOffsetToCD);
                }
            }
            catch (EndOfStreamException ex)
            {
                throw new InvalidDataException(Mobile.Utility.Lib.IO.Compression.Messages.CDCorrupt, ex);
            }
            catch (IOException ex)
            {
                throw new InvalidDataException(Mobile.Utility.Lib.IO.Compression.Messages.CDCorrupt, ex);
            }
        }

        internal void ThrowIfDisposed()
        {
            if (_isDisposed)
                throw new ObjectDisposedException(this.GetType().Name);
        }

        internal void AddEntry(ZipLibraryEntry entry)
        {
            _entries.Add(entry);

            String entryName = entry.FullName;
            if (!_entriesDictionary.ContainsKey(entryName))
            {
                _entriesDictionary.Add(entryName, entry);
            }
        }
        internal void RemoveEntry(ZipLibraryEntry entry)
        {
            _entries.Remove(entry);

            _entriesDictionary.Remove(entry.FullName);
        }

        internal void AcquireLibraryStream(ZipLibraryEntry entry)
        {
            //if a previous entry had held the stream but never wrote anything, we write their local header for them
            if (_LibraryStreamOwner != null)
            {
                if (!_LibraryStreamOwner.EverOpenedForWrite)
                {
                    _LibraryStreamOwner.WriteAndFinishLocalEntry();
                }
                else
                {
                    throw new IOException(Mobile.Utility.Lib.IO.Compression.Messages.CreateModeCreateEntryWhileOpen);
                }
            }

            _LibraryStreamOwner = entry;
        }
        internal Boolean IsStillLibraryStreamOwner(ZipLibraryEntry entry)
        {
            return _LibraryStreamOwner == entry;
        }
        internal void ReleaseLibraryStream(ZipLibraryEntry entry)
        {
            Contract.Assert(_LibraryStreamOwner == entry);

            _LibraryStreamOwner = null;
        }

        #endregion

        #region fields

        private Stream _LibraryStream;
        private ZipLibraryEntry _LibraryStreamOwner;
        private BinaryReader _LibraryReader;
        private ZipLibraryMode _mode;
        private List<ZipLibraryEntry> _entries;
        private ReadOnlyCollection<ZipLibraryEntry> _entriesCollection;
        private Dictionary<String, ZipLibraryEntry> _entriesDictionary;
        private Boolean _readEntries;
        private Boolean _leaveOpen;
        private Int64 _centralDirectoryStart; //only valid after ReadCentralDirectory
        private Boolean _isDisposed;
        private UInt32 _numberOfThisDisk; //only valid after ReadCentralDirectory
        private Int64 _expectedNumberOfEntries;
        private Stream _backingStream;
        private Byte[] _LibraryComment;

#if DEBUG_FORCE_ZIP64
        public Boolean _forceZip64;
#endif
        #endregion
    }
}
